// Copyright 2005, 2006 - Morten Nielsen (www.iter.dk)
//
// This file is part of SharpMap.
// SharpMap is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// SharpMap is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public License
// along with SharpMap; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Web;
using System.Xml;
using System.Xml.Schema;
using GeoAPI.Geometries;
using SharpMap.Layers;
using SharpMap.Rendering.Thematics;
using SharpMap.Web.Wms.Server;

namespace SharpMap.Web.Wms
{
    /// <summary>
    /// Class for generating the WmsCapabilities Xml
    /// </summary>
    public class ServerCapabilities : Capabilities
    {
        private const string WmsNamespaceUri = "http://www.opengis.net/wms";
        private const string XlinkNamespaceUri = "http://www.w3.org/1999/xlink";

        /// <summary>
        /// Generates a capabilities file from a map object for use in WMS services
        /// </summary>
        /// <remarks>The capabilities document uses the v1.3.0 OpenGIS WMS specification</remarks>
        /// <param name="map">The map to create capabilities for</param>
        /// <param name="description">Additional description of WMS</param>
        /// <param name="request">An abstraction of the <see cref="HttpContext"/> request</param>
        /// <returns>Returns XmlDocument describing capabilities</returns>
        public static XmlDocument GetCapabilities(Map map, WmsServiceDescription description, IContextRequest request)
        {
            XmlDocument capabilities = new XmlDocument();

            // Insert XML tag
            capabilities.InsertBefore(capabilities.CreateXmlDeclaration("1.0", "UTF-8", String.Empty), capabilities.DocumentElement);
            string format = String.Format("Capabilities generated by SharpMap v. {0}", Assembly.GetExecutingAssembly().GetName().Version);
            capabilities.AppendChild(capabilities.CreateComment(format));

            // Create root node
            XmlNode rootNode = capabilities.CreateNode(XmlNodeType.Element, "WMS_Capabilities", WmsNamespaceUri);
            rootNode.Attributes.Append(CreateAttribute("version", "1.3.0", capabilities));

            XmlAttribute attr = capabilities.CreateAttribute("xmlns", "xsi", "http://www.w3.org/2000/xmlns/");
            attr.InnerText = "http://www.w3.org/2001/XMLSchema-instance";
            rootNode.Attributes.Append(attr);

            rootNode.Attributes.Append(CreateAttribute("xmlns:xlink", XlinkNamespaceUri, capabilities));
            XmlAttribute attr2 = capabilities.CreateAttribute("xsi", "schemaLocation",
                                                              "http://www.w3.org/2001/XMLSchema-instance");
            attr2.InnerText = "http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd";
            rootNode.Attributes.Append(attr2);

            // Build Service node
            rootNode.AppendChild(GenerateServiceNode(ref description, capabilities));

            // Build Capability node
            XmlNode capabilityNode = GenerateCapabilityNode(map, capabilities, description.PublicAccessURL, request);
            rootNode.AppendChild(capabilityNode);

            capabilities.AppendChild(rootNode);

            //TODO: Validate output against schema
            return capabilities;
        }

        private static XmlNode GenerateServiceNode(ref WmsServiceDescription description, XmlDocument capabilities)
        {
            XmlElement serviceNode = capabilities.CreateElement("Service", WmsNamespaceUri);
            XmlNode name = CreateElement("Name", "WMS", capabilities, WmsNamespaceUri);
            serviceNode.AppendChild(name);
            XmlNode title = CreateElement("Title", description.Title, capabilities, WmsNamespaceUri);
            serviceNode.AppendChild(title);
            if (!String.IsNullOrEmpty(description.Abstract))
            {
                XmlNode @abstract = CreateElement("Abstract", description.Abstract, capabilities, WmsNamespaceUri);
                serviceNode.AppendChild(@abstract);
            }
            if (description.Keywords != null && description.Keywords.Length > 0)
            {
                XmlElement keywordListNode = capabilities.CreateElement("KeywordList", WmsNamespaceUri);
                foreach (string kw in description.Keywords)
                {
                    XmlNode keyword = CreateElement("Keyword", kw, capabilities, WmsNamespaceUri);
                    keywordListNode.AppendChild(keyword);
                }
                serviceNode.AppendChild(keywordListNode);
            }

            XmlElement onlineResource = GenerateOnlineResourceElement(capabilities, description.OnlineResource);
            serviceNode.AppendChild(onlineResource);

            XmlElement contactInfo = GenerateContactInfoElement(capabilities, description.ContactInformation);
            if (contactInfo.HasChildNodes)
                serviceNode.AppendChild(contactInfo);

            if (!String.IsNullOrEmpty(description.Fees))
            {
                XmlNode fees = CreateElement("Fees", description.Fees, capabilities, WmsNamespaceUri);
                serviceNode.AppendChild(fees);
            }
            if (!String.IsNullOrEmpty(description.AccessConstraints))
            {
                XmlNode accessConstraints = CreateElement("AccessConstraints", description.AccessConstraints, capabilities, WmsNamespaceUri);
                serviceNode.AppendChild(accessConstraints);
            }
            if (description.LayerLimit > 0)
            {
                XmlNode layerLimit = CreateElement("LayerLimit", description.LayerLimit.ToString(), capabilities, WmsNamespaceUri);
                serviceNode.AppendChild(layerLimit);
            }
            if (description.MaxWidth > 0)
            {
                XmlNode maxWidth = CreateElement("MaxWidth", description.MaxWidth.ToString(), capabilities, WmsNamespaceUri);
                serviceNode.AppendChild(maxWidth);
            }
            if (description.MaxHeight > 0)
            {
                XmlNode maxHeight = CreateElement("MaxHeight", description.MaxHeight.ToString(), capabilities, WmsNamespaceUri);
                serviceNode.AppendChild(maxHeight);
            }
            return serviceNode;
        }

        private static XmlNode GenerateCapabilityNode(Map map, XmlDocument capabilities, string publicWMSUrl, IContextRequest request)
        {
            string onlineResource;
            if (String.IsNullOrEmpty(publicWMSUrl))
            {
                string port = request.Url.IsDefaultPort ? "" : ":" + request.Url.Port;
                string s = String.Format("http://{0}{1}{2}", request.Url.Host, port,
                    request.Url.AbsolutePath);
                onlineResource = request.Encode(s);
            }
            else onlineResource = publicWMSUrl;

            XmlNode capabilityNode = capabilities.CreateNode(XmlNodeType.Element, "Capability", WmsNamespaceUri);
            XmlNode requestNode = capabilities.CreateNode(XmlNodeType.Element, "Request", WmsNamespaceUri);
            XmlNode getCapabilitiesNode = capabilities.CreateNode(XmlNodeType.Element, "GetCapabilities",
                                                                  WmsNamespaceUri);
            // Set format of supported capabilities mime types (only text/xml supported)
            getCapabilitiesNode.AppendChild(CreateElement("Format", "text/xml", capabilities, WmsNamespaceUri));
            getCapabilitiesNode.AppendChild(GenerateDCPTypeNode(capabilities, onlineResource));
            requestNode.AppendChild(getCapabilitiesNode);
            XmlNode getFeatureInfoNode = capabilities.CreateNode(XmlNodeType.Element, "GetFeatureInfo", WmsNamespaceUri);
            getFeatureInfoNode.AppendChild(CreateElement("Format", "text/plain", capabilities, WmsNamespaceUri));
            getFeatureInfoNode.AppendChild(CreateElement("Format", "text/json", capabilities, WmsNamespaceUri));
            getFeatureInfoNode.AppendChild(GenerateDCPTypeNode(capabilities, onlineResource));
            requestNode.AppendChild(getFeatureInfoNode);
            XmlNode getMapNode = capabilities.CreateNode(XmlNodeType.Element, "GetMap", WmsNamespaceUri);
            // Add supported fileformats. Return the ones that GDI+ supports
            foreach (ImageCodecInfo encoder in ImageCodecInfo.GetImageEncoders())
                getMapNode.AppendChild(CreateElement("Format", encoder.MimeType, capabilities, WmsNamespaceUri));

            getMapNode.AppendChild(GenerateDCPTypeNode(capabilities, onlineResource));

            requestNode.AppendChild(getMapNode);
            capabilityNode.AppendChild(requestNode);
            XmlElement exceptionNode = capabilities.CreateElement("Exception", WmsNamespaceUri);
            exceptionNode.AppendChild(CreateElement("Format", "text/xml", capabilities, WmsNamespaceUri));
            capabilityNode.AppendChild(exceptionNode); // Add supported exception types

            // Build layerlist
            XmlNode layerRootNode = capabilities.CreateNode(XmlNodeType.Element, "Layer", WmsNamespaceUri);
            layerRootNode.AppendChild(CreateElement("Title", "SharpMap", capabilities, WmsNamespaceUri));
            layerRootNode.AppendChild(CreateElement("CRS", "EPSG:" + map.Layers[0].SRID, capabilities, WmsNamespaceUri)); //TODO
            layerRootNode.AppendChild(GenerateBoundingBoxElement(map.GetExtents(), map.Layers[0].SRID, capabilities));

            XmlElement geoBox = capabilities.CreateElement("EX_GeographicBoundingBox", WmsNamespaceUri);
            // Using default values here. TODO: this should be changed when projection library is complete
            const string minX = "-180";
            const string minY = "-90";
            const string maxX = "180";
            const string maxY = "90";
            geoBox.AppendChild(CreateElement("westBoundLongitude", minX, capabilities, WmsNamespaceUri));
            geoBox.AppendChild(CreateElement("southBoundLatitude", minY, capabilities, WmsNamespaceUri));
            geoBox.AppendChild(CreateElement("eastBoundLongitude", maxX, capabilities, WmsNamespaceUri));
            geoBox.AppendChild(CreateElement("northBoundLatitude", maxY, capabilities, WmsNamespaceUri));

            // The above way to respresent the boundingbox is correct and replace the incorrect way below. 
            // For downward compatibility I will keep the old code.
            geoBox.Attributes.Append(CreateAttribute("minx", minX, capabilities));
            geoBox.Attributes.Append(CreateAttribute("miny", minY, capabilities));
            geoBox.Attributes.Append(CreateAttribute("maxx", maxX, capabilities));
            geoBox.Attributes.Append(CreateAttribute("maxy", maxY, capabilities));
            layerRootNode.AppendChild(geoBox);

            foreach (ILayer layer in map.Layers)
            {
                XmlNode wmsLayerNode = GetWmsLayerNode(layer, capabilities);
                layerRootNode.AppendChild(wmsLayerNode);
            }

            capabilityNode.AppendChild(layerRootNode);
            return capabilityNode;
        }

        private static XmlNode GenerateDCPTypeNode(XmlDocument capabilities, string onlineResource)
        {
            XmlNode dcpType = capabilities.CreateNode(XmlNodeType.Element, "DCPType", WmsNamespaceUri);
            XmlNode httpType = capabilities.CreateNode(XmlNodeType.Element, "HTTP", WmsNamespaceUri);
            XmlElement resource = GenerateOnlineResourceElement(capabilities, onlineResource);

            XmlNode getNode = capabilities.CreateNode(XmlNodeType.Element, "Get", WmsNamespaceUri);
            XmlNode postNode = capabilities.CreateNode(XmlNodeType.Element, "Post", WmsNamespaceUri);
            getNode.AppendChild(resource.Clone());
            postNode.AppendChild(resource);
            httpType.AppendChild(getNode);
            httpType.AppendChild(postNode);
            dcpType.AppendChild(httpType);
            return dcpType;
        }

        private static XmlElement GenerateOnlineResourceElement(XmlDocument capabilities, string onlineResource)
        {
            XmlElement resource = capabilities.CreateElement("OnlineResource", WmsNamespaceUri);
            XmlAttribute attrType = capabilities.CreateAttribute("xlink", "type", XlinkNamespaceUri);
            attrType.Value = "simple";
            resource.Attributes.Append(attrType);
            XmlAttribute href = capabilities.CreateAttribute("xlink", "href", XlinkNamespaceUri);
            href.Value = onlineResource;
            resource.Attributes.Append(href);
            XmlAttribute xmlns = capabilities.CreateAttribute("xmlns:xlink");
            xmlns.Value = XlinkNamespaceUri;
            resource.Attributes.Append(xmlns);
            return resource;
        }

        private static XmlElement GenerateContactInfoElement(XmlDocument capabilities, WmsContactInformation info)
        {
            XmlElement infoNode = capabilities.CreateElement("ContactInformation", WmsNamespaceUri);

            // Add primary person
            XmlElement cpp = capabilities.CreateElement("ContactPersonPrimary", WmsNamespaceUri);
            if (!String.IsNullOrEmpty(info.PersonPrimary.Person))
            {
                XmlNode primary = CreateElement("ContactPerson", info.PersonPrimary.Person, capabilities, WmsNamespaceUri);
                cpp.AppendChild(primary);
            }
            if (!String.IsNullOrEmpty(info.PersonPrimary.Organisation))
            {
                XmlNode organization = CreateElement("ContactOrganization", info.PersonPrimary.Organisation, capabilities, WmsNamespaceUri);
                cpp.AppendChild(organization);
            }
            if (cpp.HasChildNodes)
                infoNode.AppendChild(cpp);

            if (!String.IsNullOrEmpty(info.Position))
            {
                XmlNode position = CreateElement("ContactPosition", info.Position, capabilities, WmsNamespaceUri);
                infoNode.AppendChild(position);
            }

            // Add address
            XmlElement ca = capabilities.CreateElement("ContactAddress", WmsNamespaceUri);
            if (!string.IsNullOrEmpty(info.Address.AddressType))
            {
                XmlNode addressType = CreateElement("AddressType", info.Address.AddressType, capabilities, WmsNamespaceUri);
                ca.AppendChild(addressType);
            }
            if (!String.IsNullOrEmpty(info.Address.Address))
            {
                XmlNode address = CreateElement("Address", info.Address.Address, capabilities, WmsNamespaceUri);
                ca.AppendChild(address);
            }
            if (!String.IsNullOrEmpty(info.Address.City))
            {
                XmlNode city = CreateElement("City", info.Address.City, capabilities, WmsNamespaceUri);
                ca.AppendChild(city);
            }
            if (!String.IsNullOrEmpty(info.Address.StateOrProvince))
            {
                XmlNode state = CreateElement("StateOrProvince", info.Address.StateOrProvince, capabilities, WmsNamespaceUri);
                ca.AppendChild(state);
            }
            if (!String.IsNullOrEmpty(info.Address.PostCode))
            {
                XmlNode postCode = CreateElement("PostCode", info.Address.PostCode, capabilities, WmsNamespaceUri);
                ca.AppendChild(postCode);
            }
            if (!String.IsNullOrEmpty(info.Address.Country))
            {
                XmlNode country = CreateElement("Country", info.Address.Country, capabilities, WmsNamespaceUri);
                ca.AppendChild(country);
            }
            if (ca.HasChildNodes)
                infoNode.AppendChild(ca);

            if (!String.IsNullOrEmpty(info.VoiceTelephone))
            {
                XmlNode telephone = CreateElement("ContactVoiceTelephone", info.VoiceTelephone, capabilities, WmsNamespaceUri);
                infoNode.AppendChild(telephone);
            }
            if (!String.IsNullOrEmpty(info.FacsimileTelephone))
            {
                XmlNode facsimile = CreateElement("ContactFacsimileTelephone", info.FacsimileTelephone, capabilities, WmsNamespaceUri);
                infoNode.AppendChild(facsimile);
            }
            if (!String.IsNullOrEmpty(info.ElectronicMailAddress))
            {
                XmlNode email = CreateElement("ContactElectronicMailAddress", info.ElectronicMailAddress, capabilities, WmsNamespaceUri);
                infoNode.AppendChild(email);
            }
            return infoNode;
        }

        private static XmlNode GetWmsLayerNode(ILayer layer, XmlDocument doc)
        {
            XmlNode layerNode = doc.CreateNode(XmlNodeType.Element, "Layer", WmsNamespaceUri);
            layerNode.AppendChild(CreateElement("Name", layer.LayerName, doc, WmsNamespaceUri));
            layerNode.AppendChild(CreateElement("Title", layer.LayerName, doc, WmsNamespaceUri));
            // If this is a vector layer with themes property set, add the Styles
            if (layer is VectorLayer)
            {
                Dictionary<string, ITheme> themes = ((VectorLayer) layer).Themes;
                if (themes != null)
                {
                    foreach (KeyValuePair<string, ITheme> kvp in (layer as VectorLayer).Themes)
                    {
                        XmlNode styleNode = doc.CreateNode(XmlNodeType.Element, "Style", WmsNamespaceUri);
                        XmlNode name = CreateElement("Name", kvp.Key, doc, WmsNamespaceUri);
                        styleNode.AppendChild(name);
                        XmlNode title = CreateElement("Title", kvp.Key, doc, WmsNamespaceUri);
                        styleNode.AppendChild(title);
                        layerNode.AppendChild(styleNode);
                    }
                }
            }
            // If this is a grouplayer, add childlayers recursively
            if (layer is LayerGroup)
            {
                Collection<Layer> layers = ((LayerGroup)layer).Layers;
                foreach (Layer childlayer in layers)
                {
                    XmlNode node = GetWmsLayerNode(childlayer, doc);
                    layerNode.AppendChild(node);
                }
            }

            XmlAttribute queryable = CreateAttribute("queryable", GetQueriable(layer), doc);
            layerNode.Attributes.Append(queryable);
            XmlElement bbox = GenerateBoundingBoxElement(layer.Envelope, layer.SRID, doc);
            layerNode.AppendChild(bbox);
            return layerNode;
        }

        private static string GetQueriable(ILayer layer)
        {
            if (!(layer is ICanQueryLayer)) 
                return "0";
            var queryLayer = (ICanQueryLayer)layer;
            return queryLayer.IsQueryEnabled ? "1" : "0";
        }

        private static XmlElement GenerateBoundingBoxElement(Envelope bbox, int SRID, XmlDocument doc)
        {
            XmlElement xmlBbox = doc.CreateElement("BoundingBox", WmsNamespaceUri);
            xmlBbox.Attributes.Append(CreateAttribute("minx", bbox.Left().ToString(Map.NumberFormatEnUs), doc));
            xmlBbox.Attributes.Append(CreateAttribute("miny", bbox.Bottom().ToString(Map.NumberFormatEnUs), doc));
            xmlBbox.Attributes.Append(CreateAttribute("maxx", bbox.Right().ToString(Map.NumberFormatEnUs), doc));
            xmlBbox.Attributes.Append(CreateAttribute("maxy", bbox.Top().ToString(Map.NumberFormatEnUs), doc));
            xmlBbox.Attributes.Append(CreateAttribute("CRS", "EPSG:" + SRID, doc));
            return xmlBbox;
        }

        private static XmlAttribute CreateAttribute(string name, string value, XmlDocument doc)
        {
            XmlAttribute attr = doc.CreateAttribute(name);
            attr.Value = value;
            return attr;
        }

        private static XmlNode CreateElement(string name, string value, XmlDocument doc, string namespaceURI)
        {
            XmlNode node = doc.CreateNode(XmlNodeType.Element, name, namespaceURI);
            node.InnerXml = value;
            return node;
        }

        internal static XmlDocument CreateXml()
        {
            XmlDocument capabilities = new XmlDocument();
            // Set XMLSchema
            capabilities.Schemas = new XmlSchemaSet();
            capabilities.Schemas.Add(GetCapabilitiesSchema());

            return capabilities;
        }

        private static XmlSchema GetCapabilitiesSchema()
        {
            // Get XML Schema
            const string resource = "SharpMap.Web.Wms.Schemas._1._3._0.capabilities_1_3_0.xsd";
            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resource))
            {
                XmlSchema schema = XmlSchema.Read(stream, ValidationError);
                return schema;
            }
        }

        private static void ValidationError(object sender, ValidationEventArgs arguments) { }        
    }
}